/*
 *  SDPParse.java
 * 
 *  Created on Apr 26, 2008, 8:31:05 PM
 */
package pl.edu.agh.ssm.monitor.parsers.sdp;

import pl.edu.agh.ssm.monitor.data.SessionDescription;
import pl.edu.agh.ssm.monitor.data.SessionMedia;
import pl.edu.agh.ssm.monitor.data.SessionMediaType;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import pl.edu.agh.ssm.monitor.data.SessionType;
import pl.edu.agh.ssm.monitor.data.SessionDescription.MediaDescription;

/**
 * Simple SDP parser.
 * 
 * @author aneezka
 */
public class SDPParser {

    /**
     * @todo clean this whole mess from the class (printlns, commented lines, etc.)
     * @todo maybe better error handling?
     */
    public SDPParser() {
    }

    public SessionDescription parse(String sdpData) throws InvalidDataException {
        if (sdpData == null || sdpData.trim().equals("")) {
            throw new InvalidDataException("Empty SDP data string");
        }

        String[] params = sdpData.split("\n");
        if (params.length == 0) {
            throw new InvalidDataException("Invalid SDP data (could not recognize any parameters)");
        }

        ParameterList paramList = new ParameterList();
        for (String p : params) {
            Parameter param = parseSingleLine(p);
//            System.out.println("got parameter " + param.getType() + "          " + param.getValue());
            paramList.add(param);
        }

        for (String m : mandatory) {
            if (!paramList.contains(m)) {
                throw new InvalidDataException("Invalid SDP data (there is no mandatory parameter " + m + ")");
            }
        }

        boolean b = false;
        for (String t : paramList.getTypes()) {
            b = false;
            for (String a : all) {
                if (a.equals(t)) {
                    b = true;
                    break;
                }
            }
            if (!b) {
                throw new InvalidDataException("Invalid SDP data (unrecognized parameter: " + t + ")");
            }
        }

        SessionDescription sessionDescription = new SessionDescription();

        //Actual parsing of SDP
        //First of all, retrieve grouped parameters 
        //(the order of retrieving parameters *is* important - groups have to be parsed first and then rest of the parameters can be interpreted as "single" parameters) 
        ParameterList timeDescParams = null;
        List<String> allowedTypes = new LinkedList<String>();
        for (String s : time) {
            allowedTypes.add(s);
        }
        while ((timeDescParams = paramList.retrieveGroup(allowedTypes)) != null) {
            SessionDescription.TimeDescription timeDesc = new SessionDescription.TimeDescription();
            String t = timeDescParams.retrieveSingle("t");
            String[] times = t.split(" ");
            if (times.length != 2) {
                throw new InvalidDataException("Invalid SDP data (\"t\" parameter format: " + t + ")");
            }
            long startTime;
            long endTime;
            try {
                startTime = Long.parseLong(times[0]);
                endTime = Long.parseLong(times[1]);
            } catch (NumberFormatException numberFormatException) {
                throw new InvalidDataException("Invalid SDP data (\"t\" parameter format: " + t + ")");
            }
            if (startTime != 0L) {
                timeDesc.setStartDate(new Date(startTime * 1000L));
            }
            if (endTime != 0L) {
                timeDesc.setEndDate(new Date(endTime * 1000L));
            }

            String r = timeDescParams.retrieveSingle("r");
            if (r != null) {
                String[] rStrings = r.split(" ");
                if (rStrings.length < 3) {
                    throw new InvalidDataException("Invalid SDP data (\"r\" parameter format: " + r + ")");
                }
                timeDesc.setRepeatInterval(rStrings[0]);
                timeDesc.setActiveDuration(rStrings[1]);
                for (int i = 2; i < rStrings.length; i++) {
                    timeDesc.addOffset(rStrings[i]);
                }
            }
            sessionDescription.addTimeDescription(timeDesc);
        }

        ParameterList mediaDescParams = null;
        allowedTypes = new LinkedList<String>();
        for (String s : media) {
            allowedTypes.add(s);
        }
        while ((mediaDescParams = paramList.retrieveGroup(allowedTypes)) != null) {
//            System.out.println("Found media ");
            SessionDescription.MediaDescription mediaDesc = new SessionDescription.MediaDescription();
            mediaDesc.setMediaName(mediaDescParams.retrieveSingle("m"));
            mediaDesc.setMediaTitle(mediaDescParams.retrieveSingle("i"));
            mediaDesc.setConnectionInfo(mediaDescParams.retrieveSingle("c"));
            //mediaDesc.setBandwidth(mediaDescParams.retrieveSingle("b"));
            mediaDesc.setEncryptionKey(mediaDescParams.retrieveSingle("k"));
            String aParam = null;
            while ((aParam = mediaDescParams.retrieveSingle("b")) != null) {
                mediaDesc.addBandwidth(aParam);
            }
            while ((aParam = mediaDescParams.retrieveSingle("a")) != null) {
                mediaDesc.addMediaAttribute(aParam);
            }
            sessionDescription.addMediaDescription(mediaDesc);
        }
        
        //Parse media description

        //Now, all the left single parameters can be retrieved
        sessionDescription.setVersion(paramList.retrieveSingle("v"));
        sessionDescription.setName(paramList.retrieveSingle("s"));
        sessionDescription.setOwner(paramList.retrieveSingle("o"));
        sessionDescription.setDescription(paramList.retrieveSingle("i"));
        sessionDescription.setUri(paramList.retrieveSingle("u"));
        sessionDescription.setEmail(paramList.retrieveSingle("e"));
        sessionDescription.setPhone(paramList.retrieveSingle("p"));
        sessionDescription.setConnectionInfo(paramList.retrieveSingle("c"));
        //sessionDescription.setBandwidth(paramList.retrieveSingle("b"));
        sessionDescription.setTimeZoneAdjustment(paramList.retrieveSingle("z"));
        sessionDescription.setEncryptionKey(paramList.retrieveSingle("e"));
        //todo: add left parameters (check with RFC!)
        String attribute = null;
        while ((attribute = paramList.retrieveSingle("b")) != null) {
            sessionDescription.addBandwidth(attribute);
        }
        while ((attribute = paramList.retrieveSingle("a")) != null) {
            sessionDescription.addSessionAttribute(attribute);
        }
//        for (int i = 0; i < sessionDescription.getTimeDescriptions().size(); i++) {
//            System.out.println("Start : " + sessionDescription.getTimeDescriptions().get(i).getStartDate());
//            System.out.println("End : " + sessionDescription.getTimeDescriptions().get(i).getEndDate());
//        }
        
        sessionDescription.setType(SessionType.SDP_SESSION);
        parseMediaDetails(sessionDescription);
        
        return sessionDescription;
    }

    private void parseMediaDetails(SessionDescription sessionDescription) {
    	for(MediaDescription media : sessionDescription.getMediaDescriptions()) {
    		String[] names = media.getMediaName().split(" ");
    		/* MEDIA TYPE */
    		try {
    			media.setMediaType(SessionMediaType.valueOf(names[0].toUpperCase()));
    		} catch(Exception e) {
    			media.setMediaType(SessionMediaType.OTHER);
    		}
    		/* CONNECTION PORT */
    		try {
    			media.setMediaPort(Integer.valueOf(names[1]).intValue());
    		} catch(Exception e) {
    			media.setMediaPort(0);
    		}
    		/* PAYLOAD TYPE */
    		if(names.length > 3) {
    			try {
    				media.setPayloadType(Integer.valueOf(names[3]).intValue());
    			} catch (Exception e) {
    				media.setPayloadType(0);
    			}
    		}
    		
    		/* Connection address */
    		String connectionAddress = media.getConnectionInfo();
    		if(connectionAddress == null || connectionAddress == "") {
    			connectionAddress = sessionDescription.getConnectionInfo();
    		}
    		try {
    			String address = connectionAddress.split(" ")[2]; //[0]=IN, [1]=IP4, [2]=xxx.yyy.zzz.www
    			if(address.lastIndexOf('/') > -1 ) {
    				media.setMediaAddress(address.substring(0, address.lastIndexOf('/')));
    			} else {
    				media.setMediaAddress(address);
    			}
    		} catch(Exception e) {
    			media.setMediaAddress("0.0.0.0");
    		}
    		
    		/* Check rtpmap information */
    		for(String attribute : media.getMediaAttributes()) {
//    			System.out.println("Check attribute: " + attribute);
    			if(attribute.startsWith("rtpmap")) {
    				String tmp = attribute.substring(
    						attribute.lastIndexOf(':') + 2, attribute.length()).split(" ")[1];
    				String[] vals = tmp.split("/");
    				SessionMedia sessionMedia = new SessionMedia();
    				sessionMedia.setMediaType(media.getMediaType());
    				sessionMedia.setMediaName(vals[0]);
    				sessionMedia.setMediaFreq(Integer.valueOf(vals[1]));
    				
    				//add media desc to rtpmap in session description
    				sessionDescription.addMap(media.getPayloadType(), sessionMedia);
    				break; //no other map information for this media
    			}
    		}
    	}
    }
//    public List<SessionDescription> parseMultiple(String sdpData) {
//
//    }
    private Parameter parseSingleLine(String sdpLine) throws InvalidDataException {
        sdpLine = sdpLine.trim();
        String[] s = sdpLine.split("=", 2);
//        System.out.println("Parsing sdpLine: " + sdpLine);
//        for (String st : s) {
//            System.out.println("Got parts " + st);
//        }

        //if (s.length != 2 || s[1].trim().equals("")) {
        if (s.length != 2 ) {
            throw new InvalidDataException("Invalid SDP data (line: " + sdpLine + ")");
        }
//        System.out.println("Returning " + s[0].trim() + " " + s[1].trim());
        return new Parameter(s[0].trim(), s[1].trim());
    }

   /* private boolean isMandatory(String parameter) {
        for (String m : mandatory) {
            if (m.equals(parameter)) {
                return true;
            }
        }
        return false;
    }*/

    private class Parameter {

        public Parameter(String type, String value) {
            this.type = type;
            this.value = value;
        }

        public String getType() {
            return type;
        }

        public String getValue() {
            return value;
        }
        private String type;
        private String value;
    }

    private class ParameterList {

        public void add(Parameter parameter) {
            //parameters.add(parameter);
//            System.out.println("Adding parameter " + parameter.getType());
            values.add(parameter.getValue());
            types.add(parameter.getType());
        }

        public void add(String type, String value) {

            types.add(type);
            values.add(value);
        }

        public String retrieveSingle(String type) {
            int index = types.indexOf(type);
//            System.out.println("Retrieving single " + type);
//            System.out.println("Before " + types);
            if (index >= 0) {
                //Parameter result = parameters.get(index);
                String result = values.get(index);
                values.remove(index);
                //parameters.remove(index);
                types.remove(index);
//                System.out.println("After " + types);
                return result;
            } else {
                return null;
            }
        }

        public ParameterList retrieveGroup(List<String> allowedTypes) {
            if (allowedTypes.size() == 0) {
                return null;
            }
//            System.out.println("Retrieving group " + allowedTypes);
            String type = allowedTypes.get(0);
            int index = types.indexOf(type);
            //System.out.println("Retrieved index " + index);
//            System.out.println("Before " + types.toString());
            if (index >= 0) {
                ParameterList result = new ParameterList();
                List<Integer> toRemove = new LinkedList<Integer>();
                //result.add(parameters.get(index));
                result.add(type, values.get(index));
                toRemove.add(index);
                for (int i = index + 1; i < types.size(); i++) {
                    if (!types.get(i).equals(type) && allowedTypes.contains(types.get(i))) {
                        //result.add(parameters.get(i));
                        result.add(types.get(i), values.get(i));
                        toRemove.add(index);
                    } else {
                        break;
                    }
                }
                for (Integer r : toRemove) {
                    //parameters.remove(r.intValue());
                    values.remove(r.intValue());
                    types.remove(r.intValue());
                }
//                System.out.println("After " + types.toString());
//                System.out.println("REturning " + result);
                return result;
            } else {
//                System.out.println("Printing whole table " + types.toString());
                return null;
            }
        }

        public boolean contains(String m) {
            return types.contains(m);
        }

        public List<String> getTypes() {
            return types;
        }

        /*private int getSize() {
            return types.size();
        }*/

        List<String> values = new LinkedList<String>();
        List<String> types = new LinkedList<String>();
        List<String> groupMarks = new LinkedList<String>();
    }
    
    //TODO: add 'v' parameter to this list but first fix its parsing (remove this whole garbage in the front of package)
    private String[] mandatory = new String[]{"v", "o", "s", "t", "m"};
    private String[] media = new String[]{"m", "i", "c", "b", "k", "a"};
    private String[] time = new String[]{"t", "r"};
    private String[] all = new String[]{"v", "o", "s", "i", "u", "e", "p", "c", "b", "t", "r", "z", "k", "a", "m"};
}
